<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Materials Instancing Modified — Space.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three/build/three.module.js",
                "three/addons/": "https://unpkg.com/three/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import { Color, HemisphereLight, IcosahedronGeometry, InstancedBufferAttribute, InstancedMesh, Matrix4, MeshPhongMaterial, PerspectiveCamera, Scene, WebGLRenderer } from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';

        const DEBUG = /[?&]debug/.test(location.search);

        const amount = parseInt(location.search.slice(1), 10) || 3;
        const count = Math.pow(amount, 3);

        const color = new Color();

        // init

        const renderer = new WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        const scene = new Scene();
        scene.background = new Color(0x0e0e0e);

        const camera = new PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 2000);
        camera.position.set(amount, amount, amount);
        camera.lookAt(scene.position);

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.enableZoom = false;
        controls.enablePan = false;

        // lights

        scene.add(new HemisphereLight(0xffffff, 0x888888, 3));

        // mesh

        let geometry = new IcosahedronGeometry(0.5, 12);

        // Convert to indexed geometry
        geometry = mergeVertices(geometry);

        geometry.computeTangents();

        const material = new MeshPhongMaterial();

        const mesh = new InstancedMesh(geometry, material, count);

        let i = 0;
        const offset = (amount - 1) / 2;

        const matrix = new Matrix4();

        for (let x = 0; x < amount; x++) {
            for (let y = 0; y < amount; y++) {
                for (let z = 0; z < amount; z++) {
                    matrix.setPosition(offset - x, offset - y, offset - z);

                    mesh.setMatrixAt(i, matrix);
                    mesh.setColorAt(i, color);

                    i++;
                }
            }
        }

        scene.add(mesh);

        // panel

        import { InstanceOptions, MaterialPanelController, MaterialPanels, MaterialPatches, Panel, PanelItem, Point3D, UI } from '../../src/three.js';

        // Add attributes
        geometry.setAttribute('instanceOpacity', new InstancedBufferAttribute(new Float32Array(mesh.instanceMatrix.count).fill(1), 1));

        MaterialPatches.phong = {
            instanceOpacity(shader) {
                shader.vertexShader = shader.vertexShader.replace(
                    '#include <color_pars_vertex>',
                    /* glsl */ `
                    #include <color_pars_vertex>

                    attribute float instanceOpacity;
                    varying float vInstanceOpacity;
                    `
                );

                shader.vertexShader = shader.vertexShader.replace(
                    '#include <color_vertex>',
                    /* glsl */ `
                    #include <color_vertex>

                    vInstanceOpacity = instanceOpacity;
                    `
                );

                shader.fragmentShader = shader.fragmentShader.replace(
                    '#include <color_pars_fragment>',
                    /* glsl */ `
                    #include <color_pars_fragment>

                    varying float vInstanceOpacity;
                    `
                );

                shader.fragmentShader = shader.fragmentShader.replace(
                    '#include <color_fragment>',
                    /* glsl */ `
                    #include <color_fragment>

                    diffuseColor.a = vInstanceOpacity * opacity;
                    `
                );
            }
        };

        class InstancedMeshPanel extends Panel {
            constructor(mesh, materialItems) {
                super();

                this.mesh = mesh;
                this.materialItems = materialItems;

                this.initPanel();
            }

            initPanel() {
                const mesh = this.mesh;
                const point = Point3D.getPoint(mesh);
                const materialItems = this.materialItems;

                const items = [
                    {
                        type: 'list',
                        label: 'Instance',
                        list: InstanceOptions,
                        value: 'Mesh',
                        callback: (value, panel) => {
                            if (InstanceOptions[value]) {
                                const index = point.instances[0].index;

                                const instanceItems = [
                                    {
                                        type: 'divider'
                                    },
                                    {
                                        type: 'slider',
                                        label: 'Opacity',
                                        min: 0,
                                        max: 1,
                                        step: 0.01,
                                        value: mesh.geometry.attributes.instanceOpacity.getX(index),
                                        callback: value => {
                                            if (value < 1) {
                                                mesh.material.transparent = true;
                                                mesh.material.needsUpdate = true;
                                            }

                                            point.instances.forEach(instance => {
                                                mesh.geometry.attributes.instanceOpacity.setX(instance.index, value);
                                            });

                                            mesh.geometry.attributes.instanceOpacity.needsUpdate = true;
                                        }
                                    }
                                ];

                                const instancePanel = new Panel();
                                instancePanel.animateIn(true);

                                instanceItems.forEach(data => {
                                    instancePanel.add(new PanelItem(data));
                                });

                                panel.setContent(instancePanel);
                            } else {
                                const materialPanel = new Panel();
                                materialPanel.animateIn(true);

                                materialItems.forEach(data => {
                                    materialPanel.add(new PanelItem(data));
                                });

                                panel.setContent(materialPanel);
                            }
                        }
                    }
                ];

                items.forEach(data => {
                    this.add(new PanelItem(data));
                });
            }
        }

        MaterialPanels.InstancedMeshPanel = InstancedMeshPanel;

        const ui = new UI({ fps: true });
        ui.animateIn();
        document.body.appendChild(ui.element);

        Point3D.init(scene, camera, {
            debug: DEBUG
        });

        const point = new Point3D(mesh);
        scene.add(point);

        MaterialPanelController.init(mesh, point);

        // animation

        function animate(time) {
            requestAnimationFrame(animate);

            time = time * 0.001; // seconds

            controls.update();

            renderer.render(scene, camera);

            Point3D.update(time);
            ui.update();
        }

        requestAnimationFrame(animate);

        // resize

        window.addEventListener('resize', onWindowResize);

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();

            renderer.setSize(window.innerWidth, window.innerHeight);
        }
    </script>
</head>
<body>
</body>
</html>
